/******************************************************************************
 * Copyright (c) 2018 TypeFox and others.
 * 
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 * 
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 ******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.json.adapters;

import java.io.IOException;
import java.lang.reflect.Constructor;

import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

/**
 * A type adapter for {@link Throwable}. This is used to report issues to the sender of a request.
 */
public class ThrowableTypeAdapter extends TypeAdapter<Throwable> {
	
	public static class Factory implements TypeAdapterFactory {

		@SuppressWarnings({ "unchecked" })
		@Override
		public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
			if (!Throwable.class.isAssignableFrom(typeToken.getRawType()))
				return null;
			
			return (TypeAdapter<T>) new ThrowableTypeAdapter((TypeToken<Throwable>) typeToken);
		}

	}
	
	private final TypeToken<Throwable> typeToken;
	
	public ThrowableTypeAdapter(TypeToken<Throwable> typeToken) {
		this.typeToken = typeToken;
	}

	@SuppressWarnings("unchecked")
	@Override
	public Throwable read(JsonReader in) throws IOException {
		if (in.peek() == JsonToken.NULL) {
			in.nextNull();
			return null;
		}
		
		in.beginObject();
		String message = null;
		Throwable cause = null;
		while (in.hasNext()) {
			String name = in.nextName();
			switch (name) {
			case "message": {
				message = in.nextString();
				break;
			}
			case "cause": {
				cause = read(in);
				break;
			}
			default:
				in.skipValue();
			}
		}
		in.endObject();
		
		try {
			Constructor<Throwable> constructor;
			if (message == null && cause == null) {
				constructor = (Constructor<Throwable>) typeToken.getRawType().getDeclaredConstructor();
				return constructor.newInstance();
			} else if (message == null) {
				constructor = (Constructor<Throwable>) typeToken.getRawType().getDeclaredConstructor(Throwable.class);
				return constructor.newInstance(cause);
			} else if (cause == null) {
				constructor = (Constructor<Throwable>) typeToken.getRawType().getDeclaredConstructor(String.class);
				return constructor.newInstance(message);
			} else {
				constructor = (Constructor<Throwable>) typeToken.getRawType().getDeclaredConstructor(String.class, Throwable.class);
				return constructor.newInstance(message, cause);
			}
		} catch (NoSuchMethodException e) {
			if (message == null && cause == null)
				return new RuntimeException();
			else if (message == null)
				return new RuntimeException(cause);
			else if (cause == null)
				return new RuntimeException(message);
			else
				return new RuntimeException(message, cause);
		} catch (Exception e) {
			throw new JsonParseException(e);
		}
	}

	@Override
	public void write(JsonWriter out, Throwable throwable) throws IOException {
		if (throwable == null) {
			out.nullValue();
		} else if (throwable.getMessage() == null && throwable.getCause() != null) {
			write(out, throwable.getCause());
		} else {
			out.beginObject();
			if (throwable.getMessage() != null) {
				out.name("message");
				out.value(throwable.getMessage());
			}
			if (shouldWriteCause(throwable)) {
				out.name("cause");
				write(out, throwable.getCause());
			}
			out.endObject();
		}
	}
	
	private boolean shouldWriteCause(Throwable throwable) {
		Throwable cause = throwable.getCause();
		if (cause == null || cause.getMessage() == null || cause == throwable)
			return false;
		if (throwable.getMessage() != null && throwable.getMessage().contains(cause.getMessage()))
			return false;
		return true;
	}

}
